| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241 |
- 'use client';
- import { useState, useEffect } from 'react';
- import { useParams, useRouter } from 'next/navigation';
- import { invitationsApi, InvitationInfo } from '@/lib/api';
- import { useAuth } from '@/lib/auth-context';
- export default function InvitePage() {
- const params = useParams();
- const token = params.token as string;
- const router = useRouter();
- const { user, token: authToken } = useAuth();
- const [invitation, setInvitation] = useState<InvitationInfo | null>(null);
- const [error, setError] = useState<string | null>(null);
- const [loading, setLoading] = useState(true);
- const [accepting, setAccepting] = useState(false);
- const [accepted, setAccepted] = useState(false);
- useEffect(() => {
- invitationsApi.verify(token)
- .then(({ invitation: inv }) => setInvitation(inv))
- .catch(() => setError('This invitation is invalid or has expired.'))
- .finally(() => setLoading(false));
- }, [token]);
- const isUsedOrExpired = invitation && (invitation.alreadyMember || invitation.isExpired);
- const handleAccept = async () => {
- if (!authToken) {
- // Redirect to login with invite token, come back after
- router.push(`/login?invite_token=${token}`);
- return;
- }
- setAccepting(true);
- try {
- await invitationsApi.accept(token);
- setAccepted(true);
- // Refresh projects list after a short delay
- setTimeout(() => router.push(`/projects`), 1500);
- } catch (err) {
- setError(err instanceof Error ? err.message : 'Failed to accept invitation');
- } finally {
- setAccepting(false);
- }
- };
- if (loading) {
- return (
- <div className="min-h-screen flex items-center justify-center" style={{ background: '#0A0B14' }}>
- <div className="text-center">
- <div className="w-8 h-8 rounded-full animate-spin mx-auto mb-4"
- style={{ borderColor: '#6366F1', borderTopColor: 'transparent', borderWidth: 2 }} />
- <p className="text-sm" style={{ color: '#6B7280' }}>Verifying invitation…</p>
- </div>
- </div>
- );
- }
- if (error && !invitation) {
- return (
- <div className="min-h-screen flex items-center justify-center" style={{ background: '#0A0B14' }}>
- <div className="card p-8 max-w-sm w-full mx-4 text-center">
- <div className="w-12 h-12 rounded-full flex items-center justify-center mx-auto mb-4"
- style={{ background: 'rgba(239,68,68,0.1)' }}>
- <svg className="w-6 h-6" style={{ color: '#EF4444' }} fill="none" viewBox="0 0 24 24" stroke="currentColor">
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
- </svg>
- </div>
- <h1 className="text-lg font-semibold mb-2" style={{ color: '#F9FAFB' }}>Invalid Invitation</h1>
- <p className="text-sm mb-6" style={{ color: '#6B7280' }}>{error}</p>
- <button onClick={() => router.push('/projects')} className="btn btn-primary btn-md w-full">
- Go to Projects
- </button>
- </div>
- </div>
- );
- }
- {/* Show project info even for expired/used invitations */}
- if (isUsedOrExpired) {
- return (
- <div className="min-h-screen flex items-center justify-center" style={{ background: '#0A0B14' }}>
- <div className="card p-8 max-w-sm w-full mx-4 text-center">
- <div className="w-12 h-12 rounded-full flex items-center justify-center mx-auto mb-4"
- style={{ background: invitation!.alreadyMember ? 'rgba(34,197,94,0.1)' : 'rgba(251,191,36,0.1)' }}>
- {invitation!.alreadyMember ? (
- <svg className="w-6 h-6" style={{ color: '#22C55E' }} fill="none" viewBox="0 0 24 24" stroke="currentColor">
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
- </svg>
- ) : (
- <svg className="w-6 h-6" style={{ color: '#FBBF24' }} fill="none" viewBox="0 0 24 24" stroke="currentColor">
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
- </svg>
- )}
- </div>
- <h1 className="text-lg font-semibold mb-2" style={{ color: '#F9FAFB' }}>
- {invitation!.alreadyMember ? 'Already Joined' : 'Invitation Expired'}
- </h1>
- <p className="text-sm mb-1" style={{ color: '#6B7280' }}>
- {invitation!.alreadyMember
- ? `You're already a member of ${invitation!.projectName}.`
- : `This invitation to ${invitation!.projectName} is no longer valid.`}
- </p>
- <p className="text-xs mb-6" style={{ color: '#4B5563' }}>
- {invitation!.alreadyMember
- ? 'Visit your projects to start collaborating.'
- : 'Ask the project admin to send a new invitation.'}
- </p>
- <button
- onClick={() => user ? router.push('/projects') : router.push('/login')}
- className="btn btn-primary btn-md w-full"
- >
- {user ? 'Go to Projects' : 'Sign In'}
- </button>
- </div>
- </div>
- );
- }
- if (accepted) {
- return (
- <div className="min-h-screen flex items-center justify-center" style={{ background: '#0A0B14' }}>
- <div className="card p-8 max-w-sm w-full mx-4 text-center">
- <div className="w-12 h-12 rounded-full flex items-center justify-center mx-auto mb-4"
- style={{ background: 'rgba(34,197,94,0.1)' }}>
- <svg className="w-6 h-6" style={{ color: '#22C55E' }} fill="none" viewBox="0 0 24 24" stroke="currentColor">
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
- </svg>
- </div>
- <h1 className="text-lg font-semibold mb-2" style={{ color: '#F9FAFB' }}>Welcome!</h1>
- <p className="text-sm" style={{ color: '#6B7280' }}>
- You've joined <strong style={{ color: '#F9FAFB' }}>{invitation?.projectName}</strong>. Redirecting…
- </p>
- </div>
- </div>
- );
- }
- return (
- <div className="min-h-screen flex items-center justify-center" style={{ background: '#0A0B14' }}>
- <div className="card p-8 max-w-sm w-full mx-4">
- {/* Project icon */}
- <div className="w-12 h-12 rounded-2xl flex items-center justify-center mx-auto mb-4"
- style={{ background: 'rgba(99,102,241,0.1)' }}>
- <svg className="w-6 h-6" style={{ color: '#818CF8' }} fill="none" viewBox="0 0 24 24" stroke="currentColor">
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5}
- d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z" />
- </svg>
- </div>
- <h1 className="text-lg font-semibold text-center mb-1" style={{ color: '#F9FAFB' }}>
- You're invited
- </h1>
- <p className="text-sm text-center mb-1" style={{ color: '#9CA3AF' }}>
- to join project
- </p>
- <p className="text-xl font-bold text-center mb-6" style={{ color: '#818CF8' }}>
- {invitation?.projectName}
- </p>
- {/* Role badge */}
- <div className="flex justify-center mb-6">
- <span className="badge badge-brand capitalize">
- {invitation?.role.toLowerCase()}
- </span>
- </div>
- {invitation?.alreadyMember ? (
- <div className="text-center">
- <p className="text-sm mb-4" style={{ color: '#6B7280' }}>
- You're already a member of this project.
- </p>
- <button onClick={() => router.push('/projects')} className="btn btn-primary btn-md w-full">
- Go to Projects
- </button>
- </div>
- ) : !user ? (
- <div className="text-center">
- <p className="text-sm mb-4" style={{ color: '#6B7280' }}>
- Create an account or sign in to accept this invitation.
- </p>
- <div className="space-y-2">
- <button onClick={handleAccept} className="btn btn-primary btn-md w-full">
- Sign in to accept
- </button>
- <button
- onClick={() => router.push(`/register?invite_token=${token}`)}
- className="btn btn-secondary btn-md w-full"
- >
- Create account
- </button>
- </div>
- </div>
- ) : invitation?.isOwnInvitation ? (
- <div className="text-center">
- <p className="text-sm mb-4" style={{ color: '#6B7280' }}>
- This invitation was sent to <strong style={{ color: '#F9FAFB' }}>{user.email}</strong>
- </p>
- <button
- onClick={handleAccept}
- disabled={accepting}
- className="btn btn-primary btn-md w-full"
- >
- {accepting ? 'Joining…' : 'Accept & Join Project'}
- </button>
- </div>
- ) : (
- <div className="text-center">
- <div className="w-8 h-8 rounded-full flex items-center justify-center mx-auto mb-3"
- style={{ background: 'rgba(239,68,68,0.1)' }}>
- <svg className="w-4 h-4" style={{ color: '#EF4444' }} fill="none" viewBox="0 0 24 24" stroke="currentColor">
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
- </svg>
- </div>
- <p className="text-sm mb-2" style={{ color: '#F87171' }}>Email mismatch</p>
- <p className="text-xs mb-4" style={{ color: '#6B7280' }}>
- This invitation was sent to <strong>{invitation?.email}</strong>.<br/>
- You're currently logged in as <strong>{user.email}</strong>.
- </p>
- <p className="text-xs" style={{ color: '#4B5563' }}>
- Sign in with the correct account, or ask the project admin to resend the invitation.
- </p>
- </div>
- )}
- {/* Expiry */}
- {invitation?.expiresAt && (
- <p className="text-xs text-center mt-6" style={{ color: '#4B5563' }}>
- Expires {new Date(invitation.expiresAt).toLocaleDateString()}
- </p>
- )}
- {error && (
- <p className="text-xs text-center mt-3" style={{ color: '#F87171' }}>{error}</p>
- )}
- </div>
- </div>
- );
- }
|